跳到主要内容

Java 枚举的使用

枚举的底层原理

枚举被设计成是单例模式,即枚举类型会由 JVM 在加载的时候,实例化枚举对象,在枚举类中定义了多少个就会实例化多少个, JVM 为了保证每一个枚举类元素的唯一实例,是不会允许外部进行 new 的,所以会把构造函数设计成 private,防止用户生成实例,破坏唯一性。

枚举在很多时候会和常量拿来对比,可能因为本身我们大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢?

public class PizzaStatus {
public static final int ORDERED = 0;
public static final int READY = 1;
public static final int DELIVERED = 2;
}

以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。

下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED、READY、DELIVERED状态:

public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}

enum 类型

通过 enum 定义的枚举类,和其他的 class 有什么区别?

答案是没有任何区别。enum 定义的类型就是 class,只不过它有以下几个特点:

  • 定义的 enum 类型总是继承自 java.lang.Enum 且无法被继承;
  • 只能定义出 enum 的实例,而无法通过 new 操作符创建 enum 的实例;
  • 定义的每个实例都是引用类型的唯一实例;
  • 可以将 enum 类型用于 switch 语句。

例如,定义的 Color 枚举类:

public enum Color {
RED, GREEN, BLUE;
}

编译器编译出的 class 大概就像这样(通过反编译获得的):

public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}

所以,编译后的 enum 类和普通 class 并没有任何区别。但是我们自己无法按定义普通 class 那样来定义 enum,必须使用 enum 关键字,这是 Java 语法规定的。

因为 enum 是一个 class,每个枚举的值都是 class 实例,因此,这些实例有一些方法:

name()

返回常量名,例如:

String s = Weekday.SUN.name(); // "SUN"

ordinal()

返回定义的常量的顺序,从0开始计数,例如:

int n = Weekday.MON.ordinal(); // 1

改变枚举常量定义的顺序就会导致 ordinal() 返回值发生变化。不过不要太依赖这个方法,因为万一常量顺序改了就很麻烦了

用 == 比较枚举类型

由于枚举类型确保 JVM 中仅存在一个常量实例,因此我们可以安全地使用 == 运算符比较两个变量,如上例所示;此外,== 运算符可提供编译时和运行时的安全性。

以下使用这个类做示例

public class Pizza {
private PizzaStatus status;
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
}

首先,让我们看一下以下代码段中的运行时安全性,其中 == 运算符用于比较状态,并且如果两个值均为 null 都不会引发 NullPointerException。相反,如果使用 equals 方法,将抛出 NullPointerException:

Pizza.PizzaStatus pizza = null;
System.out.println(pizza.equals(Pizza.PizzaStatus.DELIVERED));//空指针异常
System.out.println(pizza == Pizza.PizzaStatus.DELIVERED);//正常运行

编译时安全性,我们看另一个示例,两个不同枚举类型进行比较:

if (Pizza.PizzaStatus.DELIVERED.equals(TestColor.GREEN)); // 编译正常
if (Pizza.PizzaStatus.DELIVERED == TestColor.GREEN); // 编译失败,类型不匹配

枚举里面使用 switch 语句

  public int apply(Operation operation, int arg1, int arg2) {
switch(operation) {
case ADD:
return arg1 + arg2;
case SUBTRACT:
return arg1 - arg2;
case MULTIPLY:
return arg1 * arg2;
default:
throw new UnsupportedOperationException();
}
}

上面这种用法其实有一定的问题,如果我们将一个新操作添加到我们的枚举 Operation 中,编译器不会通知我们这个开关不能正确处理新操作。

所以这种时候可以对这种传入一个枚举的操作,直接丢到枚举里面

enum Operation {
ADD,
SUBTRACT,
MULTIPLY;

public int apply(int arg1, int arg2) {
switch(this) {
case ADD:
return arg1 + arg2;
case SUBTRACT:
return arg1 - arg2;
case MULTIPLY:
return arg1 * arg2;
default:
throw new UnsupportedOperationException();
}
}
}

这样就可以直接像这样使用:

Operation.ADD.apply(2, 3);

使用函数式编程对上面的代码更进一步的简化

enum Operation {
ADD((x, y) -> x + y),
SUBTRACT((x, y) -> x - y),
MULTIPLY((x, y) -> x * y);

private final BiFunction<Integer, Integer, Integer> operation;

Operation(BiFunction<Integer, Integer, Integer> operation) {
this.operation = operation;
}

public int apply(int x, int y) {
return operation.apply(x, y);
}
}

注意,这个函数接口使用如下

class Adder implements BiFunction<Integer, Integer, String> {
@Override
public String apply(Integer x, Integer y) {
return x + y;
}
}

枚举的构造方法

public enum FruitEnum {
APPLE("RED",6),ORANGE("YELLOW",10),PEAR("YELLOW",20);
private String color;
private int day;

FruitEnum(String color, int day) {
this.color = color;
this.day = day;
}
}

注意:这个构造方法依旧是 private 修饰的,所以它依旧不能用于实例化,它的主要作用是给枚举对象添加附加信息(就是享元模式嘛,把不同的部分提取出来)

向枚举中添加新方法

如果打算自定义自己的方法,那么必须在 enum 实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。

public enum Color {  
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLOW("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
Color(String name, int index) {
this.name = name;
this.index = index;
}

// 普通方法
public static String getName(int index) {
for (Color c : Color.values()) {
if (c.getIndex() == index) {
return c.name;
}
}
return null;
}

// get set 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}

枚举的重写

前面说了,每个枚举都继承自 Enum 这个类,而每个枚举对象又是实例的它自己

public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}

所以也可以重写它的自己方法

public enum Color {
//这里每一个枚举项都可以看做一个单例实例
RED,
GREEN{
@Override
public String sayHello(String str) {
return "GREEN:"+str;
}
},
BLUE{
@Override
public String sayHello(String str) {
return "BLUE:"+str;
}
};

public String sayHello(String str) {
return "Hello:"+str;
}

}

使用接口组织枚举

enum 不能继承其他类,有时而需要扩展原 enum 中的元素。(多态)

在一个接口的内部,创建实现该接口的枚举,可以达到将枚举元素分类组织的目的。

举例来说,假设想用 enum 来表示不同类别的食物,同时还希望每个 enum 元素仍然保持 Food 类型。那么可以这样实现:


interface Food {

enum Appetizer implements Food {
SALAD, SOUP, SPRING_ROLLS;
}

enum MainCourse implements Food {
LASAGNE, BURRITO, PAD_THAI,
LENTILS, HUMMOUS, VINDALOO;
}

enum Dessert implements Food {
TIRAMISU, GELATO, BLACK_FOREST_CAKE,
FRUIT, CREME_CARAMEL;
}
}

枚举集合的使用

java.util.EnumSetjava.util.EnumMap 是两个枚举集合。

EnumSet

参考资料 EnumSet的使用及源码分析

假设一种场景,如果你想用 一个数表示多种状态,那么位运算是一种很好的选择。用或运算复合多种状态,用与运算判断是否包含某种状态。

例如如下场景: 在一些工作中(如医生、客服),不是每个工作人员每天都在的,每个人可工作的时间是不一样的,比如张三可能是周一和周三,李四可能是周四和周六,给定每个人可工作的时间,我们可能有一些问题需要回答。比如:

  • 有没有哪天一个人都不会来?
  • 有哪些天至少会有一个人来?
  • 有哪些天至少会有两个人来?
  • 有哪些天所有人都会来,以便开会?
  • 哪些人周一和周二都会来?

由此,可以使用位运算写出如下代码

public class Temp {
public static void main(String[] args) {
Worker zs = new Worker("张三", Worker.MONDAY | Worker.TUESDAY);

if((zs.getDay() & Worker.TUESDAY) > 0) {
System.out.println(zs.name + "星期二工作");
}
}

static class Worker {
public static final int MONDAY = 1; // 1
public static final int TUESDAY = 1 << 1; // 2
public static final int WEDNESDAY = 1 << 2; // 4
public static final int THURSDAY = 1 << 3; // 8
public static final int FRIDAY = 1 << 4; // 16
public static final int SATURDAY = 1 << 5; // 32
public static final int SUNDAY = 1 << 6; // 64

private String name;
private int value;

public Worker(String name, int value) {
this.name = name;
this.value = value;
}

public int getDay() {
return value;
}
}
}

但是 Java 有 EnumSet,可以优化为:

先有一个表示星期的日期枚举类

enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

再创建一个工人类

class Worker {
String name;
Set<Day> availableDays;

public Worker(String name, Set<Day> availableDays) {
this.name = name;
this.availableDays = availableDays;
}
//省略getter和setter
}

创建几个工人对象

Worker[] workers = new Worker[]{
new Worker("张三", EnumSet.of(Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.FRIDAY)),
new Worker("李四", EnumSet.of(Day.TUESDAY, Day.THURSDAY, Day.SATURDAY)),
new Worker("王五", EnumSet.of(Day.TUESDAY, Day.THURSDAY))
};

1、一个人都不来的日子,即 worker 时间并集的补集

private static void noBodyComeDay(Worker[] workers) {
Set<Day> days = EnumSet.allOf(Day.class);
for (Worker worker : workers) {
days.removeAll(worker.getAvailableDays());
}
System.out.println(days);
}

2、至少有一个人会来的日子,即 worker 时间的并集

private static void atLeastOneBodyComeDay(Worker[] workers) {
Set<Day> days = EnumSet.noneOf(Day.class);
for(Worker w : workers){
days.addAll(w.getAvailableDays());
}
System.out.println(days);
}

3、所有人都会来的日子,即 worker 时间的交集

private static void allManComeDay(Worker[] workers) {
Set<Day> days = EnumSet.allOf(Day.class);
for(Worker w : workers){
days.retainAll(w.getAvailableDays());
}
System.out.println(days);
}

4、哪些人周一和周二都会来

private static void monAndTueComeBody(Worker[] workers) {
Set<Worker> availableWorkers = new HashSet<>();
for(Worker w : workers){
if(w.getAvailableDays().containsAll(EnumSet.of(Day.MONDAY, Day.TUESDAY))){
availableWorkers.add(w);
}
}
for(Worker w : availableWorkers){
System.out.println(w.getName());
}
}

EnumMap

参考资料 使用EnumMap

因为 HashMap 是一种通过对 key 计算 hashCode(),通过空间换时间的方式,直接定位到 value 所在的内部数组的索引,因此,查找效率非常高。

如果作为 key 的对象是 enum 类型,那么,还可以使用 Java 集合库提供的一种 EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据 enum 类型的 key 直接定位到内部数组的索引,并不需要计算 hashCode(),不但效率最高,而且没有额外的空间浪费。

public class Main {
public static void main(String[] args) {
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "星期一");
map.put(DayOfWeek.TUESDAY, "星期二");
map.put(DayOfWeek.WEDNESDAY, "星期三");
map.put(DayOfWeek.THURSDAY, "星期四");
map.put(DayOfWeek.FRIDAY, "星期五");
map.put(DayOfWeek.SATURDAY, "星期六");
map.put(DayOfWeek.SUNDAY, "星期日");
System.out.println(map);
System.out.println(map.get(DayOfWeek.MONDAY));
}
}

使用 EnumMap 的时候,一般用 Map 接口来引用它,因此,实际上把 HashMap 和 EnumMap 互换,在客户端看来没有任何区别。

  • 如果 Map 的 key 是 enum 类型,推荐使用 EnumMap,既保证速度,也不浪费空间。
  • 使用 EnumMap 的时候,根据面向抽象编程的原则,应持有 Map 接口。

枚举实现单例模式

创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。

public class Temp {
public static void main(String[] args) {
Temp single = Temp.getInstance(); // 获取单例
}

private Temp() {}

public static Temp getInstance() {
return Singleton.INSTANCE.getInstance();
}

// 枚举是线程安全的,并且只会装载一次
private enum Singleton {
INSTANCE;
private final Temp instance;

Singleton() {
instance = new Temp();
}

private Temp getInstance() {
return instance;
}
}
}

枚举实现策略模式

通常,策略模式由不同类实现同一个接口来实现的。

这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。

下面的代码段显示了如何使用枚举实现策略模式:

public enum CalculatorEnum {

ADD("+") {
@Override
public int exec(int a, int b) {
return a + b;
}
},
SUB("-") {
@Override
public int exec(int a, int b) {
return a - b;
}
};

CalculatorEnum(String s) {
this.value = s;
}

private final String value;

public abstract int exec(int a, int b);
}

使用

@Test
public void testCalculatorEnum() {
int addResult = CalculatorEnum.ADD.exec(1, 3);
int subResult = CalculatorEnum.SUB.exec(2, 1);
assertEquals(4, addResult);
assertEquals(1, subResult);
}

Enum 转 JSON 的问题

参考资料 一种将枚举Enum转换为JSON对象的方法

使用 Jackson 库,可以将枚举类型的 JSON 表示为 POJO。

但是 Jackson ObjectMapper 默认将枚举类型 Enum 转换为它的名称,亦即为字符串,比如将枚举 SUCCESS(“SUCCESS”, 200) 输出为“SUCCESS”,这丢失了很多额外的信息,并且前端也不易处理,如将服务器返回状态定义为枚举:

public enum ActionStatus {

SUCCESS("SUCCESS", 200), FAIL_500("FAIL", 500);

private final String status;

private final int code;

private ActionStatus(String status, int code) {
this.status = status;
this.code = code;
}

public String getStatus() {
return status;
}

public int getCode() {
return code;
}

}

而在枚举中,默认的 toString 方法是不能覆盖的,只能手动进行处理:

  1. 在 ActionStatus 添加转换为 Map 的方法;
  2. 实现 JsonSerializer 接口,注册类型序列化方法
//  将此方法添加在ActionStatus中
public Map<String, Object> toMap() {
return ImmutableMap.<String, Object>builder()
.put("status", status)
.put("code", code)
.build();
}

实现序列化接口:

public class ActionStatusSerializer extends JsonSerializer<ActionStatus> {

/* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(ActionStatus value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
Optional<ActionStatus> data = Optional.of(value);
if (data.isPresent()) {
jgen.writeObject(data.get().toMap());
} else {
jgen.writeString("");
}
}

}

最后注册到 ObjectMapper 的 SerializerByType属性,即可实现JSON对象输出,如下:

{"status":"SUCCESS","code":200}

Reference

参考资料 用好Java中的枚举真的没有那么简单 参考资料 枚举类